import { CodeBlockHighlighter } from '@blocksuite/affine/blocks/code';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { ColorScheme } from '@blocksuite/affine/model';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { type BlockStdScope } from '@blocksuite/affine/std';
import {
  type BlockSnapshot,
  nanoid,
  type SliceSnapshot,
  Text,
} from '@blocksuite/affine/store';
import type { NotificationService } from '@blocksuite/affine-shared/services';
import {
  CodeBlockIcon,
  CopyIcon,
  DownloadIcon,
  PageIcon,
  ToolIcon,
} from '@blocksuite/icons/lit';
import type { Signal } from '@preact/signals-core';
import { effect, signal } from '@preact/signals-core';
import { css, html, LitElement, nothing } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { bundledLanguagesInfo, type ThemedToken } from 'shiki';

import { preprocessHtml } from '../../utils/html';
import { ArtifactTool } from './artifact-tool';
import type { ToolError } from './type';

interface CodeArtifactToolCall {
  type: 'tool-call';
  toolCallId: string;
  toolName: string; // 'code_artifact'
  args: { title: string };
}

interface CodeArtifactToolResult {
  type: 'tool-result';
  toolCallId: string;
  toolName: string; // 'code_artifact'
  args: { title: string };
  result:
    | {
        title: string;
        html: string;
        size: number;
      }
    | ToolError
    | null;
}

export class CodeHighlighter extends SignalWatcher(WithDisposable(LitElement)) {
  static override styles = css`
    .code-highlighter {
    }

    /* Container */
    .code-highlighter pre {
      position: relative;
      margin: 0;
      display: flex;
      overflow: auto;
      font-family: ${unsafeCSSVar('fontMonoFamily')};
    }

    /* Line numbers */
    .code-highlighter .line-numbers {
      user-select: none;
      text-align: right;
      line-height: 20px;
      color: ${unsafeCSSVarV2('text/secondary')};
      white-space: nowrap;
      min-width: 3rem;
      padding: 0 0 12px 12px;
      font-size: 12px;
    }

    .code-highlighter .line-number {
      display: block;
      white-space: nowrap;
    }

    /* Code area */
    .code-highlighter :is(.code-container, .code-container-hidden) {
      flex: 1;
      white-space: pre;
      line-height: 20px;
      font-size: 12px;
      padding: 0 12px 12px 12px;
    }

    .code-highlighter .code-container {
      user-select: none;
    }

    .code-highlighter .code-container-hidden {
      color: transparent;
      position: absolute;
      top: 0;
      left: 60px;
    }

    .code-highlighter .code-line {
      display: flex;
      min-height: 20px;
    }
  `;

  @property({ attribute: false })
  accessor std!: BlockStdScope;

  @property({ attribute: false })
  accessor code: string = '';

  @property({ attribute: false })
  accessor language: string = 'html';

  @property({ attribute: false })
  accessor showLineNumbers: boolean = false;

  // signal holding tokens generated by shiki
  highlightTokens: Signal<ThemedToken[][]> = signal([]);

  get highlighter() {
    return this.std.get(CodeBlockHighlighter);
  }

  override connectedCallback() {
    super.connectedCallback();

    this.highlighter.mounted();

    // recompute highlight when code / language changes
    this.disposables.add(
      effect(() => {
        return this._updateHighlightTokens();
      })
    );
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    this.highlighter.unmounted();
  }

  private _updateHighlightTokens() {
    let cancelled = false;
    const language = this.language;
    const highlighter = this.highlighter.highlighter$.value;

    if (!highlighter) return;

    const updateTokens = () => {
      if (cancelled) return;
      requestIdleCallback(() => {
        this.highlightTokens.value = highlighter.codeToTokensBase(this.code, {
          lang: language,
          theme: this.highlighter.themeKey,
        });
      });
    };

    const loadedLanguages = highlighter.getLoadedLanguages();
    if (!loadedLanguages.includes(language)) {
      const matchedInfo = bundledLanguagesInfo.find(
        info =>
          info.id === language ||
          info.name === language ||
          info.aliases?.includes(language)
      );

      if (matchedInfo) {
        highlighter
          .loadLanguage(matchedInfo.import)
          .then(updateTokens)
          .catch(console.error);
      } else {
        console.warn(`Language not supported: ${language}`);
      }
    } else {
      updateTokens();
    }

    return () => {
      cancelled = true;
    };
  }

  private _tokenStyle(token: ThemedToken): string {
    let result = '';
    if (token.color) {
      result += `color: ${token.color};`;
    }
    if (token.fontStyle) {
      result += `font-style: ${token.fontStyle};`;
    }
    if (token.bgColor) {
      result += `background-color: ${token.bgColor};`;
    }
    return result;
  }

  override render() {
    const tokens = this.highlightTokens.value;
    const lineCount =
      tokens.length > 0 ? tokens.length : this.code.split('\n').length;

    const lineNumbersTemplate = this.showLineNumbers
      ? html`<div class="line-numbers">
          ${Array.from(
            { length: lineCount },
            (_, i) => html`<span class="line-number">${i + 1}</span>`
          )}
        </div>`
      : nothing;

    const renderedCode =
      tokens.length === 0
        ? this.code
        : html`${tokens.map(lineTokens => {
            const line = lineTokens.map(token => {
              const style = this._tokenStyle(token);
              return html`<span style="${style}">${token.content}</span>`;
            });
            return html`<div class="code-line">${line}</div>`;
          })}`;

    return html`<div class="code-highlighter">
      <pre>
        ${lineNumbersTemplate}
        <div class="code-container-hidden">${this.code}</div>
        <div class="code-container">${renderedCode}</div>
      </pre>
    </div>`;
  }
}

const CodeBlockBanner = html`<svg
  width="204"
  height="102"
  viewBox="0 0 204 102"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <g clip-path="url(#clip0_3371_100809)">
    <g filter="url(#filter0_d_3371_100809)">
      <rect
        x="53.5054"
        width="111.999"
        height="99.5543"
        rx="12.4443"
        transform="rotate(8.37805 53.5054 0)"
        fill="white"
      />
    </g>
    <path
      d="M89.7547 40.6581C90.8629 39.8345 92.4285 40.065 93.2522 41.1732C94.0758 42.2813 93.8452 43.847 92.7371 44.6706L79.7618 54.3146L89.4058 67.2899L89.5482 67.5024C90.1977 68.5905 89.9295 70.0161 88.8906 70.7883C87.8516 71.56 86.4104 71.4044 85.5558 70.4689L85.3932 70.2732L74.2581 55.2907C73.4345 54.1826 73.6653 52.617 74.7732 51.7933L89.7547 40.6581ZM114.378 44.2845C115.486 43.4608 117.052 43.6914 117.875 44.7996L129.011 59.7812C129.834 60.8892 129.604 62.4551 128.496 63.2787L113.514 74.4147L113.301 74.5552C112.213 75.2046 110.789 74.9382 110.016 73.8996C109.244 72.8606 109.399 71.4184 110.335 70.5637L110.531 70.4012L123.507 60.7572L113.863 47.7819C113.039 46.6738 113.27 45.1081 114.378 44.2845Z"
      fill="#F3F3F3"
    />
  </g>
  <defs>
    <filter
      id="filter0_d_3371_100809"
      x="35.6787"
      y="-3.32129"
      width="131.951"
      height="121.453"
      filterUnits="userSpaceOnUse"
      color-interpolation-filters="sRGB"
    >
      <feFlood flood-opacity="0" result="BackgroundImageFix" />
      <feColorMatrix
        in="SourceAlpha"
        type="matrix"
        values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
        result="hardAlpha"
      />
      <feOffset />
      <feGaussianBlur stdDeviation="2.5" />
      <feComposite in2="hardAlpha" operator="out" />
      <feColorMatrix
        type="matrix"
        values="0 0 0 0 0.258824 0 0 0 0 0.254902 0 0 0 0 0.286275 0 0 0 0.17 0"
      />
      <feBlend
        mode="normal"
        in2="BackgroundImageFix"
        result="effect1_dropShadow_3371_100809"
      />
      <feBlend
        mode="normal"
        in="SourceGraphic"
        in2="effect1_dropShadow_3371_100809"
        result="shape"
      />
    </filter>
    <clipPath id="clip0_3371_100809">
      <rect width="204" height="102" fill="white" />
    </clipPath>
  </defs>
</svg>`;

const CodeBlockBannerDark = html`<svg
  width="204"
  height="102"
  viewBox="0 0 204 102"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <g clip-path="url(#clip0_3371_101118)">
    <g filter="url(#filter0_d_3371_101118)">
      <rect
        x="53.5055"
        width="111.999"
        height="99.5543"
        rx="12.4443"
        transform="rotate(8.37805 53.5055 0)"
        fill="#252525"
      />
    </g>
    <path
      d="M89.7551 40.6574C90.8631 39.8342 92.429 40.0647 93.2525 41.1725C94.0762 42.2806 93.8455 43.8472 92.7373 44.6709L79.762 54.3149L89.406 67.2902L89.5475 67.5025C90.197 68.5907 89.9298 70.0163 88.8908 70.7886C87.8519 71.5603 86.4106 71.4047 85.5561 70.4692L85.3934 70.2735L74.2574 55.2908C73.4341 54.1829 73.6649 52.6171 74.7725 51.7934L89.7551 40.6574ZM114.378 44.2838C115.486 43.4606 117.052 43.6911 117.876 44.7988L129.011 59.7814C129.834 60.8895 129.604 62.4552 128.496 63.2788L113.514 74.4149L113.301 74.5553C112.213 75.2045 110.789 74.9381 110.016 73.8998C109.244 72.8609 109.398 71.4186 110.334 70.5638L110.532 70.4014L123.507 60.7574L113.863 47.7822C113.039 46.674 113.27 45.1074 114.378 44.2838Z"
      fill="#565656"
    />
  </g>
  <defs>
    <filter
      id="filter0_d_3371_101118"
      x="35.6787"
      y="-3.32129"
      width="131.951"
      height="121.453"
      filterUnits="userSpaceOnUse"
      color-interpolation-filters="sRGB"
    >
      <feFlood flood-opacity="0" result="BackgroundImageFix" />
      <feColorMatrix
        in="SourceAlpha"
        type="matrix"
        values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
        result="hardAlpha"
      />
      <feOffset />
      <feGaussianBlur stdDeviation="2.5" />
      <feComposite in2="hardAlpha" operator="out" />
      <feColorMatrix
        type="matrix"
        values="0 0 0 0 0.258824 0 0 0 0 0.254902 0 0 0 0 0.286275 0 0 0 0.17 0"
      />
      <feBlend
        mode="normal"
        in2="BackgroundImageFix"
        result="effect1_dropShadow_3371_101118"
      />
      <feBlend
        mode="normal"
        in="SourceGraphic"
        in2="effect1_dropShadow_3371_101118"
        result="shape"
      />
    </filter>
    <clipPath id="clip0_3371_101118">
      <rect width="204" height="102" fill="white" />
    </clipPath>
  </defs>
</svg> `;

/**
 * Component to render code artifact tool call/result inside chat.
 */
export class CodeArtifactTool extends ArtifactTool<
  CodeArtifactToolCall | CodeArtifactToolResult
> {
  static override styles = css`
    .code-artifact-preview {
      overflow: hidden;
      position: absolute;
      padding: 0;
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: column;
    }

    .code-artifact-preview > html-preview {
      height: 100%;
    }

    .code-artifact-preview > code-highlighter {
      height: 100%;
      overflow: auto;
    }

    .code-artifact-preview :is(.html-preview-iframe, .html-preview-container) {
      height: 100%;
    }

    .code-artifact-control-btn {
      background: transparent;
      border-radius: 8px;
      border: 1px solid ${unsafeCSSVarV2('button/innerBlackBorder')};
      cursor: pointer;
      font-size: 15px;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      padding: 0 8px;
      height: 32px;
      font-weight: 500;
    }

    .code-artifact-control-btn:hover {
      background: ${unsafeCSSVarV2('switch/buttonBackground/hover')};
    }

    /* Toggle styles (migrated from PreviewButton) */
    .code-artifact-toggle-container {
      display: flex;
      padding: 2px;
      align-items: flex-start;
      gap: 4px;
      border-radius: 4px;
      background: ${unsafeCSSVarV2('segment/background')};
    }

    .code-artifact-toggle-container .toggle-button {
      display: flex;
      padding: 0px 4px;
      justify-content: center;
      align-items: center;
      gap: 4px;
      border-radius: 4px;
      color: ${unsafeCSSVarV2('text/primary')};
      font-family: Inter;
      font-size: 12px;
      font-style: normal;
      font-weight: 500;
      line-height: 20px;
      cursor: pointer;
    }

    .code-artifact-toggle-container .toggle-button:hover {
      background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
    }

    .code-artifact-toggle-container .toggle-button.active {
      background: ${unsafeCSSVarV2('segment/button')};
      box-shadow:
        var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px)
          var(--Shadow-buttonShadow-1-blur, 1px) 0px
          var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)),
        var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px)
          var(--Shadow-buttonShadow-2-blur, 5px) 0px
          var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12));
    }
  `;

  @property({ attribute: false })
  accessor std: BlockStdScope | undefined;

  @property({ attribute: false })
  accessor notificationService!: NotificationService;

  @state()
  private accessor mode: 'preview' | 'code' = 'preview';

  /* ---------------- ArtifactTool hooks ---------------- */

  protected getBanner(theme: ColorScheme) {
    return theme === ColorScheme.Dark ? CodeBlockBannerDark : CodeBlockBanner;
  }

  protected getCardMeta() {
    return {
      title: this.data.args.title,
      className: 'code-artifact-result',
    };
  }

  protected override getIcon() {
    return CodeBlockIcon();
  }

  protected override getPreviewContent() {
    if (this.data.type !== 'tool-result' || !this.data.result) {
      return html``;
    }

    const result = this.data.result;
    if (typeof result !== 'object' || !('html' in result)) return html``;

    const { html: htmlContent } = result as { html: string };

    return html`<div class="code-artifact-preview">
      ${this.mode === 'preview'
        ? html`<html-preview .html=${htmlContent}></html-preview>`
        : html`<code-highlighter
            .std=${this.std}
            .code=${htmlContent}
            .language=${'html'}
            .showLineNumbers=${true}
          ></code-highlighter>`}
    </div>`;
  }

  get clipboard() {
    return this.std?.clipboard;
  }

  protected override getPreviewControls() {
    if (this.data.type !== 'tool-result' || !this.std || !this.data.result) {
      return undefined;
    }

    const result = this.data.result as { html: string; title: string };
    const htmlContent = result.html;
    const title = result.title;

    const copyHTML = async () => {
      const codeBlock: BlockSnapshot = {
        type: 'block',
        id: nanoid(),
        flavour: 'affine:code',
        version: 1,
        props: {
          language: 'html',
          wrap: false,
          caption: '',
          text: {
            '$blocksuite:internal:text$': true,
            delta: [{ insert: htmlContent }],
          },
        },
        children: [],
      };

      const sliceSnapshot: SliceSnapshot = {
        type: 'slice',
        content: [codeBlock],
        workspaceId: 'fake-workspace-id',
        pageId: 'fake-page-id',
      };

      await this.clipboard?.writeToClipboard(items => ({
        ...items,
        'text/plain': htmlContent,
        'text/html': htmlContent,
        'BLOCKSUITE/SNAPSHOT': JSON.stringify({
          snapshot: sliceSnapshot,
          blobs: {},
        }),
      }));
      this.notificationService.toast('Copied HTML to clipboard');
    };

    const downloadHTML = () => {
      try {
        const blob = new Blob([htmlContent], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${title || 'artifact'}.html`;
        document.body.append(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
      } catch (e) {
        console.error(e);
      }
    };

    const insertHtmlBlockToEnd = () => {
      try {
        const store = this.std?.store;
        if (!store) return;
        const notes = store.getBlocksByFlavour('affine:note');
        const parentId = notes.length > 0 ? notes[0].id : store.root?.id;
        if (!parentId) return;
        const html = preprocessHtml(htmlContent);
        store.addBlock(
          'affine:code',
          { text: new Text(html), language: 'html', preview: true },
          parentId
        );
        this.notificationService.toast('Inserted to current doc');
      } catch (e) {
        console.error(e);
      }
    };

    const setCodeMode = () => {
      if (this.mode !== 'code') {
        this.mode = 'code';
        this.refreshPreviewPanel();
      }
    };

    const setPreviewMode = () => {
      if (this.mode !== 'preview') {
        this.mode = 'preview';
        this.refreshPreviewPanel();
      }
    };

    return html`
      <div class="code-artifact-toggle-container">
        <div
          class=${classMap({
            'toggle-button': true,
            active: this.mode === 'code',
          })}
          @click=${setCodeMode}
        >
          Code
        </div>
        <div
          class=${classMap({
            'toggle-button': true,
            active: this.mode === 'preview',
          })}
          @click=${setPreviewMode}
        >
          Preview
        </div>
      </div>
      <div style="flex: 1"></div>
      <button class="code-artifact-control-btn" @click=${insertHtmlBlockToEnd}>
        ${PageIcon({
          width: '20',
          height: '20',
          style: `color: ${unsafeCSSVarV2('icon/primary')}`,
        })}
        Insert
      </button>
      <icon-button @click=${downloadHTML} title="Download HTML">
        ${DownloadIcon({ width: '20', height: '20' })}
      </icon-button>
      <icon-button @click=${copyHTML} title="Copy HTML">
        ${CopyIcon({ width: '20', height: '20' })}
      </icon-button>
    `;
  }

  protected override getErrorTemplate() {
    if (
      this.data.type === 'tool-result' &&
      this.data.result &&
      (this.data.result as any).type === 'error'
    ) {
      return html`<tool-call-failed
        .name=${'Code artifact failed'}
        .icon=${ToolIcon()}
      ></tool-call-failed>`;
    }
    return null;
  }
}
